sysroot: Support boot counting for boot entries
authorIgor Opaniuk <igor.opaniuk@foundries.io>
Wed, 11 Sep 2024 16:03:10 +0000 (18:03 +0200)
committerIgor Opaniuk <igor.opaniuk@foundries.io>
Thu, 10 Jul 2025 06:58:19 +0000 (08:58 +0200)
Add support for boot counting for bootloader entries [1].
The boot counting data is stored in the name of the boot loader entry.
A boot loader entry file name may contain a plus (+) followed by a number.
This may optionally be followed by a minus (-) followed by a second number.
The dot (.) and file name suffix (conf or efi) must immediately follow.

The feature is enabled via sysroot configuration:
[sysroot]
boot-counting-tries=3

Testing:
$ ostree admin deploy 91fc19319be9e79d07159303dff125f40f10e5c25614630dcbed23d95e36f907
Copying /etc changes: 2 modified, 3 removed, 4 added
bootfs is sufficient for calculated new size: 0 bytes
Transaction complete; bootconfig swap: yes; bootversion: boot.0.1, deployment count change: 1

$ ls /boot/loader/entries
ostree-1.conf  ostree-2+3.conf

[1] https://uapi-group.org/specifications/specs/boot_loader_specification/#boot-counting
Signed-off-by: Igor Opaniuk <igor.opaniuk@foundries.io>
Signed-off-by: Colin Walters <walters@verbum.org>
13 files changed:
Makefile-libostree.am
Makefile-tests.am
apidoc/ostree-sections.txt
man/ostree.repo-config.xml
src/libostree/libostree-devel.sym
src/libostree/ostree-bootconfig-parser-private.h [new file with mode: 0644]
src/libostree/ostree-bootconfig-parser.c
src/libostree/ostree-bootconfig-parser.h
src/libostree/ostree-repo-private.h
src/libostree/ostree-repo.c
src/libostree/ostree-sysroot-deploy.c
tests/test-admin-boot-counting-tries.sh [new file with mode: 0755]
tests/test-bootconfig-parser-internals.c [new file with mode: 0644]

index 463b809a1cdae44b8d7f19883688f1cce5f2c670..5a17e44648ccad96a1b6c094824bd086022bc0d0 100644 (file)
@@ -110,6 +110,7 @@ libostree_1_la_SOURCES = \
        src/libostree/ostree-soft-reboot.c \
        src/libostree/ostree-impl-system-generator.c \
        src/libostree/ostree-bootconfig-parser.c \
+       src/libostree/ostree-bootconfig-parser-private.h \
        src/libostree/ostree-deployment.c \
        src/libostree/ostree-bootloader.h \
        src/libostree/ostree-bootloader.c \
index 57695e18aba90b5383d193592e6bac712c31db1c..f5e15c2322930ba7e94d3b8878b04ccaa52c1598 100644 (file)
@@ -122,6 +122,7 @@ _installed_or_uninstalled_test_scripts = \
        tests/test-osupdate-dtb.sh \
        tests/test-admin-instutil-set-kargs.sh \
        tests/test-admin-upgrade-not-backwards.sh \
+       tests/test-admin-boot-counting-tries.sh \
        tests/test-admin-pull-deploy-commit.sh \
        tests/test-admin-pull-deploy-split.sh \
        tests/test-admin-locking.sh \
@@ -280,7 +281,7 @@ endif
 
 _installed_or_uninstalled_test_programs = tests/test-varint tests/test-ot-unix-utils tests/test-bsdiff tests/test-otcore tests/test-mutable-tree \
        tests/test-keyfile-utils tests/test-ot-opt-utils tests/test-ot-tool-util \
-       tests/test-checksum tests/test-lzma tests/test-rollsum \
+       tests/test-checksum tests/test-lzma tests/test-rollsum tests/test-bootconfig-parser-internals \
        tests/test-basic-c tests/test-sysroot-c tests/test-pull-c tests/test-repo tests/test-include-ostree-h tests/test-kargs \
        tests/test-rfc2616-dates tests/test-pem
 
@@ -348,6 +349,10 @@ tests_test_kargs_SOURCES = src/libostree/ostree-kernel-args.c tests/test-kargs.c
 tests_test_kargs_CFLAGS = $(TESTS_CFLAGS)
 tests_test_kargs_LDADD = $(TESTS_LDADD)
 
+tests_test_bootconfig_parser_internals_SOURCES = tests/test-bootconfig-parser-internals.c
+tests_test_bootconfig_parser_internals_CFLAGS = $(TESTS_CFLAGS)
+tests_test_bootconfig_parser_internals_LDADD = $(TESTS_LDADD)
+
 tests_test_repo_finder_config_SOURCES = tests/test-repo-finder-config.c
 tests_test_repo_finder_config_CFLAGS = $(TESTS_CFLAGS)
 tests_test_repo_finder_config_LDADD = $(TESTS_LDADD)
index a43d1079293b03e6d2ddd43898051d67fc6b2ac2..7e02d2f66af12cc9f33a28600004dbdd5f16cd05 100644 (file)
@@ -39,6 +39,8 @@ ostree_bootconfig_parser_set
 ostree_bootconfig_parser_get
 ostree_bootconfig_parser_set_overlay_initrds
 ostree_bootconfig_parser_get_overlay_initrds
+ostree_bootconfig_parser_get_tries_left
+ostree_bootconfig_parser_get_tries_done
 <SUBSECTION Standard>
 OSTREE_BOOTCONFIG_PARSER
 OSTREE_IS_BOOTCONFIG_PARSER
index d1c2e34e7256ea0da4dd032fc9a57a2749e1ddb7..a9cc75bd2b78335e1cc8fac9f46f5162fbf2f9e6 100644 (file)
@@ -408,6 +408,18 @@ License along with this library. If not, see <https://www.gnu.org/licenses/>.
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>boot-counting-tries</varname></term>
+        <listitem><para>Integer value controlling the number of maximum boot attempts. The boot
+        counting data is stored in the name of the boot loader entry. A boot loader entry file name
+        may contain a plus (+) followed by a number. This may optionally be followed by
+        a minus (-) followed by a second number. The dot (.) and file name suffix (conf or efi) must
+        immediately follow. More details in the
+        <ulink url="https://uapi-group.org/specifications/specs/boot_loader_specification/#boot-counting">
+        The Boot Loader Specification</ulink>
+        </para></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>bls-append-except-default</varname></term>
         <listitem><para>A semicolon separated string list of key-value pairs. For example:
index 610a36b7e990e4aa95edf84062c20b8357c6b92d..ce58121d630cf0bec4ab7137cb7cc9b60a90f428 100644 (file)
@@ -36,4 +36,6 @@ global:
   ostree_sysroot_deployment_can_soft_reboot;
   ostree_sysroot_deployment_set_soft_reboot;
   ostree_sysroot_clear_soft_reboot;
+  ostree_bootconfig_parser_get_tries_left;
+  ostree_bootconfig_parser_get_tries_done;
 } LIBOSTREE_2025.2;
diff --git a/src/libostree/ostree-bootconfig-parser-private.h b/src/libostree/ostree-bootconfig-parser-private.h
new file mode 100644 (file)
index 0000000..16ccb0f
--- /dev/null
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.0+ */
+
+#pragma once
+
+#include "ostree-bootconfig-parser.h"
+
+G_BEGIN_DECLS
+
+const char *_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self);
+
+G_END_DECLS
index 4c3e80d083d2a3638ed14f550dde76a5ff75dbe6..2e08256d7207c3a6502817aa60a3717b16a084e0 100644 (file)
 
 #include "config.h"
 
-#include "ostree-bootconfig-parser.h"
+#include "ostree-bootconfig-parser-private.h"
 #include "otutil.h"
 
 struct _OstreeBootconfigParser
 {
   GObject parent_instance;
 
-  gboolean parsed;
+  char *filename;
   const char *separators;
 
+  guint64 tries_left;
+  guint64 tries_done;
+
   GHashTable *options;
 
   /* Additional initrds; the primary initrd is in options. */
@@ -51,11 +54,88 @@ ostree_bootconfig_parser_clone (OstreeBootconfigParser *self)
   GLNX_HASH_TABLE_FOREACH_KV (self->options, const char *, k, const char *, v)
     g_hash_table_replace (parser->options, g_strdup (k), g_strdup (v));
 
+  parser->filename = g_strdup (self->filename);
   parser->overlay_initrds = g_strdupv (self->overlay_initrds);
 
   return parser;
 }
 
+/*
+ * Parses a suffix of two counters in the form "+LEFT-DONE" from the end of the
+ * filename (excluding file extension).
+ */
+static void
+parse_bootloader_tries (const char *filename, guint64 *out_left, guint64 *out_done)
+{
+  *out_left = 0;
+  *out_done = 0;
+
+  const char *counter = strrchr (filename, '+');
+  if (!counter)
+    return;
+  counter += 1;
+
+  guint64 tries_left = 0;
+  guint64 tries_done = 0;
+
+  // Negative numbers are invalid
+  if (*counter == '-')
+    return;
+
+  {
+    char *endp = NULL;
+    tries_left = g_ascii_strtoull (counter, &endp, 10);
+    if (endp == counter || (tries_left == G_MAXUINT64 && errno == ERANGE))
+      return;
+    counter = endp;
+  }
+
+  /* Parse done counter only if present */
+  if (*counter == '-')
+    {
+      counter += 1;
+      char *endp = NULL;
+      tries_done = g_ascii_strtoull (counter, &endp, 10);
+      if (endp == counter || (tries_done == G_MAXUINT64 && errno == ERANGE))
+        return;
+    }
+
+  *out_left = tries_left;
+  *out_done = tries_done;
+}
+
+/**
+ * ostree_bootconfig_parser_get_tries_left:
+ * @self: Parser
+ *
+ * Returns: Amount of boot tries left
+ *
+ * Since: 2025.2
+ */
+guint64
+ostree_bootconfig_parser_get_tries_left (OstreeBootconfigParser *self)
+{
+  return self->tries_left;
+}
+
+/**
+ * ostree_bootconfig_parser_get_tries_done:
+ * @self: Parser
+ *
+ * Returns: Amount of boot tries
+ */
+guint64
+ostree_bootconfig_parser_get_tries_done (OstreeBootconfigParser *self)
+{
+  return self->tries_done;
+}
+
+const char *
+_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self)
+{
+  return self->filename;
+}
+
 /**
  * ostree_bootconfig_parser_parse_at:
  * @self: Parser
@@ -70,7 +150,7 @@ gboolean
 ostree_bootconfig_parser_parse_at (OstreeBootconfigParser *self, int dfd, const char *path,
                                    GCancellable *cancellable, GError **error)
 {
-  g_assert (!self->parsed);
+  g_assert (!self->filename);
 
   g_autofree char *contents = glnx_file_get_contents_utf8_at (dfd, path, NULL, cancellable, error);
   if (!contents)
@@ -116,8 +196,10 @@ ostree_bootconfig_parser_parse_at (OstreeBootconfigParser *self, int dfd, const
       self->overlay_initrds = (char **)g_ptr_array_free (g_steal_pointer (&overlay_initrds), FALSE);
     }
 
-  self->parsed = TRUE;
+  const char *basename = glnx_basename (path);
+  parse_bootloader_tries (basename, &self->tries_left, &self->tries_done);
 
+  self->filename = g_strdup (basename);
   return TRUE;
 }
 
@@ -262,6 +344,7 @@ ostree_bootconfig_parser_finalize (GObject *object)
 {
   OstreeBootconfigParser *self = OSTREE_BOOTCONFIG_PARSER (object);
 
+  g_free (self->filename);
   g_strfreev (self->overlay_initrds);
   g_hash_table_unref (self->options);
 
index 5fdad72eed21af1922b7304a6f418ed29bc0510c..1ef7b7cb86b582fb5c0c5646fc3df8a04d86922b 100644 (file)
@@ -67,4 +67,10 @@ void ostree_bootconfig_parser_set_overlay_initrds (OstreeBootconfigParser *self,
 _OSTREE_PUBLIC
 char **ostree_bootconfig_parser_get_overlay_initrds (OstreeBootconfigParser *self);
 
+_OSTREE_PUBLIC
+guint64 ostree_bootconfig_parser_get_tries_left (OstreeBootconfigParser *self);
+
+_OSTREE_PUBLIC
+guint64 ostree_bootconfig_parser_get_tries_done (OstreeBootconfigParser *self);
+
 G_END_DECLS
index 1e27fc772da9412bb566cdd1108b9634674ca983..3ea1ff65a6bcf9395944a571ad3a27d51ba500b0 100644 (file)
@@ -247,6 +247,7 @@ struct OstreeRepo
   GHashTable
       *bls_append_values;     /* Parsed key-values from bls-append-except-default key in config. */
   gboolean enable_bootprefix; /* If true, prepend bootloader entries with /boot */
+  guint boot_counting;
 
   OstreeRepo *parent_repo;
 };
index 4e2f47d0e73aa5f38ffab79eeb112028c7c14c30..ec13a218c7bbcce8ef0ed99264e1165690033ff4 100644 (file)
@@ -3297,6 +3297,16 @@ reload_remote_config (OstreeRepo *self, GCancellable *cancellable, GError **erro
 static gboolean
 reload_sysroot_config (OstreeRepo *self, GCancellable *cancellable, GError **error)
 {
+  g_autofree char *boot_counting_str = NULL;
+
+  if (!ot_keyfile_get_value_with_default_group_optional (
+          self->config, "sysroot", "boot-counting-tries", "0", &boot_counting_str, error))
+    return FALSE;
+  guint64 v;
+  if (!g_ascii_string_to_unsigned (boot_counting_str, 10, 0, 5, &v, error))
+    return glnx_prefix_error (error, "Parsing sysroot.boot-counting-tries");
+  self->boot_counting = (guint)v;
+
   g_autofree char *bootloader = NULL;
 
   if (!ot_keyfile_get_value_with_default_group_optional (self->config, "sysroot", "bootloader",
index 98d296a2900d97d61c9fe8e8185ac6d01dba9992..740bc69f53a517530e90f57a9e8f5c0023ffdba8 100644 (file)
@@ -23,6 +23,7 @@
 #include <gio/gunixinputstream.h>
 #include <gio/gunixoutputstream.h>
 #include <glib-unix.h>
+#include <inttypes.h>
 #include <linux/kexec.h>
 #include <stdbool.h>
 #include <stdint.h>
@@ -37,6 +38,7 @@
 #endif
 
 #include "libglnx.h"
+#include "ostree-bootconfig-parser-private.h"
 #include "ostree-core-private.h"
 #include "ostree-deployment-private.h"
 #include "ostree-linuxfsutil.h"
@@ -1805,6 +1807,7 @@ static char *
 bootloader_entry_filename (OstreeSysroot *sysroot, guint n_deployments,
                            OstreeDeployment *deployment)
 {
+  g_autofree char *bootconf_name = NULL;
   guint index = n_deployments - ostree_deployment_get_index (deployment);
   // Allow opt-out to dropping the stateroot in case of compatibility issues.
   // As of 2024.5, we have a new naming scheme because grub2 parses the *filename* and ignores
@@ -1813,12 +1816,28 @@ bootloader_entry_filename (OstreeSysroot *sysroot, guint n_deployments,
   if (use_old_naming)
     {
       const char *stateroot = ostree_deployment_get_osname (deployment);
-      return g_strdup_printf ("ostree-%d-%s.conf", index, stateroot);
+      bootconf_name = g_strdup_printf ("ostree-%d-%s", index, stateroot);
     }
   else
     {
-      return g_strdup_printf ("ostree-%d.conf", index);
+      bootconf_name = g_strdup_printf ("ostree-%d", index);
     }
+
+  if (!sysroot->repo->boot_counting)
+    return g_strdup_printf ("%s.conf", bootconf_name);
+
+  guint max_tries = sysroot->repo->boot_counting;
+  OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment);
+
+  if (!_ostree_bootconfig_parser_filename (bootconfig))
+    return g_strdup_printf ("%s+%u.conf", bootconf_name, max_tries);
+  else if (!ostree_bootconfig_parser_get_tries_left (bootconfig)
+           && !ostree_bootconfig_parser_get_tries_done (bootconfig))
+    return g_strdup_printf ("%s.conf", bootconf_name);
+  else
+    return g_strdup_printf ("%s+%" PRIu64 "-%" PRIu64 ".conf", bootconf_name,
+                            ostree_bootconfig_parser_get_tries_left (bootconfig),
+                            ostree_bootconfig_parser_get_tries_done (bootconfig));
 }
 
 /* Given @deployment, prepare it to be booted; basically copying its
@@ -1855,7 +1874,6 @@ install_deployment_kernel (OstreeSysroot *sysroot, int new_bootversion,
   const char *bootcsum = ostree_deployment_get_bootcsum (deployment);
   g_autofree char *bootcsumdir = g_strdup_printf ("ostree/%s-%s", osname, bootcsum);
   g_autofree char *bootconfdir = g_strdup_printf ("loader.%d/entries", new_bootversion);
-  g_autofree char *bootconf_name = bootloader_entry_filename (sysroot, n_deployments, deployment);
 
   if (!glnx_shutil_mkdir_p_at (sysroot->boot_fd, bootcsumdir, 0775, cancellable, error))
     return FALSE;
@@ -2162,8 +2180,11 @@ install_deployment_kernel (OstreeSysroot *sysroot, int new_bootversion,
   if (!glnx_opendirat (sysroot->boot_fd, bootconfdir, TRUE, &bootconf_dfd, error))
     return FALSE;
 
+  g_autofree char *bootconf_filename
+      = bootloader_entry_filename (sysroot, n_deployments, deployment);
+
   if (!ostree_bootconfig_parser_write_at (ostree_deployment_get_bootconfig (deployment),
-                                          bootconf_dfd, bootconf_name, cancellable, error))
+                                          bootconf_dfd, bootconf_filename, cancellable, error))
     return FALSE;
 
   return TRUE;
@@ -4302,7 +4323,7 @@ ostree_sysroot_deployment_set_kargs_in_place (OstreeSysroot *self, OstreeDeploym
       OstreeBootconfigParser *new_bootconfig = ostree_deployment_get_bootconfig (deployment);
       ostree_bootconfig_parser_set (new_bootconfig, "options", kargs_str);
 
-      g_autofree char *bootconf_name
+      g_autofree char *bootconf_filename
           = bootloader_entry_filename (self, self->deployments->len, deployment);
 
       g_autofree char *bootconfdir = g_strdup_printf ("loader.%d/entries", self->bootversion);
@@ -4310,7 +4331,7 @@ ostree_sysroot_deployment_set_kargs_in_place (OstreeSysroot *self, OstreeDeploym
       if (!glnx_opendirat (self->boot_fd, bootconfdir, TRUE, &bootconf_dfd, error))
         return FALSE;
 
-      if (!ostree_bootconfig_parser_write_at (new_bootconfig, bootconf_dfd, bootconf_name,
+      if (!ostree_bootconfig_parser_write_at (new_bootconfig, bootconf_dfd, bootconf_filename,
                                               cancellable, error))
         return FALSE;
     }
diff --git a/tests/test-admin-boot-counting-tries.sh b/tests/test-admin-boot-counting-tries.sh
new file mode 100755 (executable)
index 0000000..b5e5c1a
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.0+
+
+set -euo pipefail
+
+. $(dirname $0)/libtest.sh
+
+setup_os_repository "archive" "syslinux"
+
+${CMD_PREFIX} ostree --repo=sysroot/ostree/repo config set sysroot.boot-counting-tries 3
+v=$(${CMD_PREFIX} ostree --repo=sysroot/ostree/repo config get sysroot.boot-counting-tries)
+assert_streq "$v" 3
+
+tap_ok "init boot counting tries"
+
+${CMD_PREFIX} ostree --repo=sysroot/ostree/repo pull-local --remote=testos testos-repo testos/buildmain/x86_64-runtime
+rev=$(${CMD_PREFIX} ostree --repo=sysroot/ostree/repo rev-parse testos/buildmain/x86_64-runtime)
+export rev
+
+${CMD_PREFIX} ostree admin deploy --karg=quiet --stateroot=testos testos:testos/buildmain/x86_64-runtime
+entry=$(ls sysroot/boot/loader/entries/)
+assert_streq "${entry}" ostree-1+3.conf
+
+tap_ok "deploy with boot counting"
+
+tap_end
diff --git a/tests/test-bootconfig-parser-internals.c b/tests/test-bootconfig-parser-internals.c
new file mode 100644 (file)
index 0000000..00b18d4
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.0+
+ */
+
+#include "config.h"
+#define _OSTREE_PUBLIC
+#include "../src/libostree/ostree-bootconfig-parser.c"
+
+static void
+test_parse_tries_valid (void)
+{
+  guint64 left, done;
+  parse_bootloader_tries ("foo", &left, &done);
+  g_assert_cmpuint (left, ==, 0);
+  g_assert_cmpuint (done, ==, 0);
+
+  parse_bootloader_tries ("foo+1", &left, &done);
+  g_assert_cmpuint (left, ==, 1);
+  g_assert_cmpuint (done, ==, 0);
+
+  parse_bootloader_tries ("foo+1-2", &left, &done);
+  g_assert_cmpuint (left, ==, 1);
+  g_assert_cmpuint (done, ==, 2);
+
+  parse_bootloader_tries ("foo+1-2.conf", &left, &done);
+  g_assert_cmpuint (left, ==, 1);
+  g_assert_cmpuint (done, ==, 2);
+}
+
+static void
+test_parse_tries_invalid (void)
+{
+  guint64 left, done;
+
+  parse_bootloader_tries ("foo+1-", &left, &done);
+  g_assert_cmpuint (left, ==, 0);
+  g_assert_cmpuint (done, ==, 0);
+
+  parse_bootloader_tries ("foo+-1", &left, &done);
+  g_assert_cmpuint (left, ==, 0);
+  g_assert_cmpuint (done, ==, 0);
+
+  parse_bootloader_tries ("foo+1-a", &left, &done);
+  g_assert_cmpuint (left, ==, 0);
+  g_assert_cmpuint (done, ==, 0);
+
+  parse_bootloader_tries ("foo+a-1", &left, &done);
+  g_assert_cmpuint (left, ==, 0);
+  g_assert_cmpuint (done, ==, 0);
+}
+
+int
+main (int argc, char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/bootconfig-parser/tries/valid", test_parse_tries_valid);
+  g_test_add_func ("/bootconfig-parser/tries/invalid", test_parse_tries_invalid);
+  return g_test_run ();
+}